﻿/*
This project and all of its code is/are Copyright (C) 2017  Nils Hunter Alving

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
using DynamicTypeBuilder;
using DynamicDependencyContainer.Interfaces;
using DynamicDependencyContainer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace DynamicDependencyContainer.Containers
{
    /// <summary>
    /// A dependency injection container that dynamically creates data types from interface declarations
    /// </summary>
    public class DynamicUnityContainer : IDynamicUnityContainer
    {
        private List<IRegisteredType> _registeredTypes;

        /// <summary>
        /// A dependency injection container that dynamically creates data types from interface declarations
        /// </summary>
        public DynamicUnityContainer()
        {
            _registeredTypes = new List<IRegisteredType>();
        }

        /// <summary>
        /// Registers the specified type as the type to return when the specified interface is resolved. A new type will be 
        /// returned each time Resolve() is called, using the default constructor
        /// </summary>
        /// <typeparam name="TI">An interface type</typeparam>
        /// <typeparam name="TT">A class type that implements the interface</typeparam>
        public void RegisterType<TI, TT>() where TT : TI where TI : class
        {
            AddOrReplaceRegisteredType(new RegisteredType<TI>(typeof(TI), typeof(TT), this));
        }

        /// <summary>
        /// Registers the specified type as the type to return when the specified interface is resolved. The constructor
        /// with the matching parameter set (type and order) will be invoked each time Resolve() is called, and a new instance
        /// of the object will be returned
        /// </summary>
        /// <typeparam name="TI">An interface type</typeparam>
        /// <typeparam name="TT">A class type that implements the interface</typeparam>
        /// <param name="constructorParameters">The parameters to be passed to the constructor each time the object is resolved</param>
        public void RegisterType<TI, TT>(object[] constructorParameters) where TT : TI where TI : class
        {
            AddOrReplaceRegisteredType(new RegisteredType<TI>(typeof(TI), typeof(TT), constructorParameters, this));
        }

        /// <summary>
        /// Registers the specified type as the type to return when the specified interface is resolved. A new instance
        /// of the object will be created using the default constructor and returned each time Resovle() is called, 
        /// after it is passed to the initializer function
        /// </summary>
        /// <typeparam name="TI">An interface type</typeparam>
        /// <typeparam name="TT">A class type that implements the interface</typeparam>
        /// <param name="initializerFunction">A function that will initialize properties on the new object instance</param>
        public void RegisterType<TI, TT>(Action<TI> initializerFunction) where TT : TI where TI : class
        {
            AddOrReplaceRegisteredType(new RegisteredType<TI>(typeof(TT), initializerFunction, this));
        }

        /// <summary>
        /// Registers the specified type as the type to return when the specified interface is resolved. The constructor
        /// with the matching parameter set (type and order) will be invoked each time Resolve() is called, and a new instance
        /// of the object will be returned after it is passed to the initializer function
        /// </summary>
        /// <typeparam name="TI">An interface type</typeparam>
        /// <typeparam name="TT">A class type that implements the interface</typeparam>
        /// <param name="constructorParameters">The parameters to be passed to the constructor each time the object is resolved</param>
        /// <param name="initializerFunction">A function that will initialize properties on the new object instance</param>
        public void RegisterType<TI, TT>(object[] constructorParameters, Action<TI> initializerFunction) where TT : TI where TI : class
        {
            AddOrReplaceRegisteredType(new RegisteredType<TI>(typeof(TT), initializerFunction, constructorParameters, this));
        }

        /// <summary>
        /// Registers the specified instance as the singleton to return when the specified interface is Resolve()'d
        /// </summary>
        /// <typeparam name="TI">An interface type</typeparam>
        /// <param name="instance">An instance of an object that implements that interface (should not be null)</param>
        /// <remarks>The type must implement the declared interface, or a runtime exception is thrown</remarks>
        public void RegisterInstance<TI>(object instance) where TI : class
        {
            AddOrReplaceRegisteredType(new RegisteredType<TI>(typeof(TI), instance, this));
        }

        /// <summary>
        /// Allows registration of a type to represent an interface, using the singleton pattern.
        /// The singleton is not created until the first time it is Resolve()'d
        /// Class must have a default constructor
        /// </summary>
        public void RegisterLazyInstance<TI, TT>() where TI : class
        {
            AddOrReplaceRegisteredType(new LazyInstanceRegisteredType<TI>(typeof(TI), typeof(TT), this));
        }

        /// <summary>
        /// Allows registration of a type to represent an interface, using the singleton pattern.
        /// The singleton is not created until the first time it is Resolve()'d
        /// </summary>
        /// <param name="constructorParams">The parameters to be passed to the constructor. The class must have a constructor
        /// with a parameter list that matches both the type and order of the specified parameters</param>
        public void RegisterLazyInstance<TI, TT>(object[] constructorParams) where TT : TI where TI : class
        {
            if (constructorParams == null)
            {
                throw new ArgumentNullException("constructorParams");
            }
            AddOrReplaceRegisteredType(new LazyInstanceRegisteredType<TI>(typeof(TI), typeof(TT), constructorParams, this));
        }

        /// <summary>
        /// Allows registration of a type to represent an interface, using the singleton pattern.
        /// The singleton is not created until the first time it is Resolve()'d
        /// </summary>
        /// <param name="initializerFunction">A function that will initialize properties on the new object instance</param>
        public void RegisterLazyInstance<TI, TT>(Action<TI> initializerFunction) where TT : TI where TI : class
        {
            AddOrReplaceRegisteredType(new LazyInstanceRegisteredType<TI>(typeof(TI), typeof(TT), initializerFunction, this));
        }

        /// <summary>
        /// Allows registration of a type to represent an interface, using the singleton pattern.
        /// The singleton is not created until the first time it is Resolve()'d
        /// </summary>
        /// <param name="initializerFunction">A function that will initialize properties on the new object instance</param>
        /// <param name="constructorParams">The parameters to be passed to the constructor. The class must have a constructor
        /// with a parameter list that matches both the type and order of the specified parameters</param>
        public void RegisterLazyInstance<TI, TT>(object[] constructorParams, Action<TI> initializerFunction) where TT : TI where TI : class
        {
            if (constructorParams == null)
            {
                throw new ArgumentNullException("constructorParams");
            }
            AddOrReplaceRegisteredType(new LazyInstanceRegisteredType<TI>(typeof(TI), typeof(TT), initializerFunction, constructorParams, this));
        }

        /// <summary>
        /// Dynamically creates types from the interface declarations. Limited to interfaces that declare properties only, no methods.
        /// Useful for eliminating the need for creating data classes to mirror your interfaces. 
        /// </summary>
        /// <param name="interfaceTypes">The list of interfaces to be registered as types</param>
        public void RegisterInterfacesAsDataTypes(List<Type> interfaceTypes)
        {
            var assemblyName = Guid.NewGuid().ToString();
            TypeFactory.CreateDynamicAssembly(assemblyName, interfaceTypes);
            var assembly = Assembly.Load(assemblyName);
            interfaceTypes.ForEach(it =>
            {
                var type = assembly.GetType($"{assemblyName}.{it.Name}");
                AddOrReplaceRegisteredType(new RegisteredInterfaceType(it, type, this));
            });
        }

        /// <summary>
        /// Returns either the singleton that was registered or a new instance of the type that was registered to
        /// represent the specified interface. The new instance will be created using whatever constructor and parameters were
        /// specified when the type was registered
        /// </summary>
        /// <typeparam name="T">An interface that was previously registered</typeparam>
        /// <returns>An object that implements the interface</returns>
        public T Resolve<T>() where T : class
        {
            return Resolve(typeof(T)) as T;
        }

        /// <summary>
        /// Returns an instance of the type that was registered to represent the specified interface. How it will be resolved 
        /// is determined by how it was originally registered
        /// </summary>
        /// <param name="type">The type of the interface to be resolved</param>
        /// <returns>An object that implements the interface</returns>
        public object Resolve(Type type)
        {
            var registeredType = ResolveRegisteredType(type);

            return registeredType.Resolve();
        }

        /// <summary>
        /// Returns an instance of the type that was registered to represent the specified interface. How it will be resolved 
        /// is determined by how it was originally registered. Instance will be passed to the initializer function before it is returned
        /// </summary>
        /// <typeparam name="T">An interface that was previously registered</typeparam>
        /// <param name="initializerFunction">A function that accepts the instance as a parameter. Use this function to intialize properties on the instance</param>
        /// <returns>An initialized instance of T</returns>
        public T Resolve<T>(Action<T> initializerFunction) where T : class
        {
            var instance = Resolve<T>();
            initializerFunction(instance);

            return instance;
        }

        /// <summary>
        /// Returns an instance of the type that was registered to represent the specified interface. How it will be resolved 
        /// is determined by how it was originally registered. Instance will be passed to the initializer function before it is returned
        /// </summary>
        /// <param name="type">The type of the interface to be resolved</param>
        /// <param name="initializerFunction">A function that accepts the instance as a parameter. Use this function to intialize properties on the instance</param>
        /// <returns>An initialized instance of the specified type</returns>
        public object Resolve(Type type, Action<object> initializerFunction)
        {
            var instance = Resolve(type);
            initializerFunction(instance);

            return instance;
        }

        private IRegisteredType ResolveRegisteredType(Type type)
        {
            ValidateTypeArgument(type);
            var registeredType = _registeredTypes.FirstOrDefault(r => r.CanResolveType(type));
            if (registeredType == null)
            {
                throw new ArgumentException($"Type: {type.Name} has not been registered with the unity container");
            }
            return registeredType;
        }

        private Type ValidateTypeArgument(Type type)
        {
            if (!type.IsInterface)
            {
                throw new ArgumentException($"Cannot resolve type {type.Name} as it is not an interface");
            }
            return type;
        }

        private void AddOrReplaceRegisteredType(IRegisteredType registeredType)
        {
            var existingRegistration = _registeredTypes.FirstOrDefault(rt => rt.CanResolveType(registeredType.RegisteredInterface));
            if (existingRegistration != null)
            {
                _registeredTypes.Remove(existingRegistration);
            }

            _registeredTypes.Add(registeredType);
        }
    }
}
